package org.rcgwt.client.ui.calendar.renderer.impl;

import java.util.Date;

import org.rcgwt.client.ui.calendar.controller.CalendarAction;
import org.rcgwt.client.ui.calendar.controller.CalendarChangesMask;
import org.rcgwt.client.ui.calendar.controller.CalendarController;
import org.rcgwt.client.ui.calendar.event.ModelChangedEvent;
import org.rcgwt.client.ui.calendar.event.ModelChangedHandler;
import org.rcgwt.client.ui.calendar.i18n.CalendarUtils;
import org.rcgwt.client.ui.calendar.i18n.ConstantsProvider;
import org.rcgwt.client.ui.calendar.model.impl.CalendarModel;
import org.rcgwt.client.ui.calendar.model.impl.SelectionCardinality;
import org.rcgwt.client.ui.calendar.model.impl.SelectionType;
import org.rcgwt.client.ui.calendar.mvc.ICalendarContext;
import org.rcgwt.client.ui.calendar.renderer.ICalendarWeeksRenderer;
import org.rcgwt.client.ui.themes.ITheme;

import com.allen_sauer.gwt.log.client.Log;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.MouseWheelEvent;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.HTMLTable.Cell;

/**
 * A Calendar week column renderer
 * 
 * @author Elian ORIOU
 * 
 */

@SuppressWarnings("deprecation")
public class CalendarWeekColumnRenderer implements ICalendarWeeksRenderer {

	private static final ConstantsProvider constants = GWT
			.create(ConstantsProvider.class);
	private static final String WEEK_PREFIX = constants.week();

	private Grid grid;

	private CalendarController controller;
	private CalendarModel model;
	private ITheme theme;

	private int previousSelectedRow = 0;

	private boolean isShiftKeyDown;
	private boolean isControlKeyDown;

	public CalendarWeekColumnRenderer() {
		isShiftKeyDown = false;
		isControlKeyDown = false;
	}

	/**
	 * Component rendering method
	 */

	@Override
	public Widget render(ICalendarContext context) {

		controller = context.getController();
		model = controller.getModel();
		theme = context.getTheme();

		grid = new Grid(7, 1);
		grid.addStyleName(theme.getCalendarWeekGridStyle().getClassName());

		createHandlers();
		fillGrid(model.getDisplayedYear() - 1900, controller.getModel()
				.getDisplayedMonth());

		return grid;
	}

	/**
	 * Creates and manages handlers
	 */

	private void createHandlers() {
		grid.addClickHandler(new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				Cell c = grid.getCellForEvent(event);
				if (c == null) {
					return;
				}
				int row = c.getRowIndex();
				int cell = c.getCellIndex();
				manageGridSelection(row, cell);
			}
		});
		controller.addModelChangedHandler(new ModelChangedHandler() {
			@Override
			public void onModelChangedEvent(ModelChangedEvent event) {
				if ((event.getElementType() & CalendarChangesMask.CLEAR_GRID) > 0) {
					clearGrid();
				}
				if ((event.getElementType() & CalendarChangesMask.SELECT_WEEK) > 0) {
					selectCell(model.getSelectedWeek(), 0);
				}
				if ((event.getElementType() & CalendarChangesMask.UNSELECT_WEEK) > 0) {
					deselectCell(model.getSelectedWeek(), 0);
				}
				if ((event.getElementType() & CalendarChangesMask.UNSELECT_WEEK_RANGE) > 0) {
					deselectRowRange(0, getLastFilledLine());
				}
				if ((event.getElementType() & (CalendarChangesMask.REFRESH_GRID
						| CalendarChangesMask.SPECIFIED_MONTH | CalendarChangesMask.SPECIFIED_YEAR)) > 0) {
					if (event.getDate() == null) {
						return;
					}
					clearGrid();
					fillGrid(event.getDate().getYear(), event.getDate()
							.getMonth());
				}
			}
		});
	}

	@Override
	public void update(ICalendarContext context) {
	}

	/**
	 * Manages grid selection
	 * 
	 * @param row
	 * @param cell
	 */

	private void manageGridSelection(int row, int cell) {
		if (model.getSelectionCardinality() != SelectionCardinality.MULTIPLE) {
			Log.warn("Multiple selection is not authorized !");
			return;
		}
		if (isShiftKeyDown == false) {
			previousSelectedRow = row;
		}
		/* MULTIPLE SELECTION (with CONTROL key) */
		if (isControlKeyDown == false) {
			controller.fireModelChangedEvent(
					CalendarChangesMask.UNFOCUS_SELECTION, null);
			controller.fireModelChangedEvent(
					CalendarChangesMask.CLEAR_SELECTION, null);
			deselectCell(model.getSelectedWeek(), 0);
			controller.handle(CalendarAction.UNSELECT_WEEK);
		}
		/* MULTIPLE SELECTION (with SHIFT key) */
		if (isShiftKeyDown == true) {
			selectRowRange(previousSelectedRow, row);
			controller.setSelectedStartWeek(previousSelectedRow);
			controller.setSelectedEndWeek(row);
			controller.handle(CalendarAction.SELECT_WEEK_RANGE);
			return;
		}
		controller.setSelectionType(SelectionType.WEEK);
		controller.setSelectedWeekLine(row);
		if (isSelected(row, cell) == true) {
			controller.handle(CalendarAction.UNSELECT_WEEK);
		} else {
			controller.handle(CalendarAction.SELECT_WEEK_CURSOR);
		}
	}

	/**
	 * Clears grid
	 */

	private void clearGrid() {
		grid.clear();
	}

	/**
	 * Fills the weeks grid
	 * 
	 * @param year
	 * @param month
	 */

	private void fillGrid(int year, int month) {
		int i = 2;
		Date d = new Date();
		d.setMonth(month);
		d.setYear(year);
		d.setDate(1);
		boolean first = true;

		Label l = new Label(WEEK_PREFIX
				+ CalendarUtils.getWeekNumber(year + 1900, month, CalendarUtils
						.getFirstMonday(year, month)));
		if (d.getDay() > 1) {
			l = new Label(WEEK_PREFIX
					+ CalendarUtils.getWeekNumber(year + 1900, month, 1));
			first = false;
		}

		if (model.getSelectionCardinality() == SelectionCardinality.NONE
				|| model.getSelectionCardinality() == SelectionCardinality.SINGLE) {
			l.addStyleName(theme.getDisabledGridCellStyle().getClassName());
		} else {
			l.addStyleName(theme.getWeeksStyle().getClassName());
		}
		grid.setWidget(1, 0, l);

		int[] mondays = CalendarUtils.getAllMondays(year, month);
		for (int j = 0; j < mondays.length; j++) {
			int m = mondays[j];
			if (m == 0) {
				continue;
			}
			if (j == 0 && first == true) {
				continue;
			}
			l = new Label(WEEK_PREFIX
					+ CalendarUtils.getWeekNumber(year + 1900, month, m));
			if (model.getSelectionCardinality() == SelectionCardinality.NONE
					|| model.getSelectionCardinality() == SelectionCardinality.SINGLE) {
				l.addStyleName(theme.getDisabledGridCellStyle()
						.getClassName());
			} else {
				l.addStyleName(theme.getWeeksStyle().getClassName());
			}
			grid.setWidget(i++, 0, l);
		}
		int day = CalendarUtils.getDaysNumber(year, month);
		Date last = new Date(year, month, day);
		if (last.getDay() == 0) {
			Label lab = new Label(WEEK_PREFIX
					+ CalendarUtils.getWeekNumber(year + 1900, month, day));
			if (model.getSelectionCardinality() == SelectionCardinality.NONE
					|| model.getSelectionCardinality() == SelectionCardinality.SINGLE) {
				lab.addStyleName(theme.getDisabledGridCellStyle()
						.getClassName());
			} else {
				lab.addStyleName(theme.getWeeksStyle().getClassName());
			}
			grid.setWidget(grid.getRowCount() - 1, 0, lab);
		}
	}

	/**
	 * Returns the last filled line in the grid
	 * 
	 * @return
	 */

	private int getLastFilledLine() {
		for (int i = 1; i < grid.getRowCount(); i++) {
			if (grid.getWidget(i, 0) == null) {
				if (i == grid.getRowCount() - 1) {
					return i - 1;
				}
				return i - 2;
			}
		}
		return 0;
	}

	/**
	 * Deselects a week row range
	 * 
	 * @param begin
	 * @param end
	 */

	private void deselectRowRange(int begin, int end) {
		if (begin <= end) {
			for (int i = begin; i <= end; i++) {
				deselectCell(i, 0);
			}
		} else {
			for (int i = end; i <= begin; i++) {
				deselectCell(i, 0);
			}
		}
	}

	/**
	 * Selects a week row range
	 * 
	 * @param begin
	 * @param end
	 */

	private void selectRowRange(int begin, int end) {
		if (begin <= end) {
			for (int i = begin; i <= end; i++) {
				selectCell(i, 0);
			}
		} else {
			for (int i = end; i <= begin; i++) {
				selectCell(i, 0);
			}
		}
	}

	/**
	 * Selects a cell (by its position in the grid)
	 * 
	 * @param row
	 * @param cell
	 */

	private void selectCell(int row, int cell) {
		Label l = (Label) grid.getWidget(row, cell);
		if (l == null) {
			return;
		}
		l.addStyleName(theme.getSelectedWeeksStyle().getClassName());
	}

	/**
	 * Deselects a cell (by its position in the grid)
	 * 
	 * @param row
	 * @param cell
	 */

	private void deselectCell(int row, int cell) {
		Label l = (Label) grid.getWidget(row, cell);
		if (l == null) {
			return;
		}
		l.removeStyleName(theme.getSelectedWeeksStyle().getClassName());
	}

	/**
	 * Returns true if the label is selected (in the context of the component)
	 * 
	 * @param row
	 * @param cell
	 * @return
	 */

	private boolean isSelected(int row, int cell) {
		Label l = (Label) grid.getWidget(row, cell);
		if (l == null) {
			return false;
		}
		return l.getStyleName().contains(
				theme.getSelectedWeeksStyle().getClassName());
	}

	/**
	 * Selects the previous week
	 */

	private void previousWeek() {
		int selection = model.getSelectedWeek();
		int month = model.getDisplayedMonth();
		int year = model.getDisplayedYear();
		if (isControlKeyDown == false) {
			deselectCell(selection, 0);
			controller.handle(CalendarAction.UNSELECT_WEEK);
		}
		if (selection > 1) {
			controller.decreaseSelectedWeek();
		} else {
			controller.handle(CalendarAction.PREVIOUS_MONTH);
			controller.setSelectedWeekLine(CalendarUtils.getWeekNumberInMonth(
					month, year) - 1);
		}
		selectCell(model.getSelectedWeek(), 0);
		controller.handle(CalendarAction.SELECT_WEEK_CURSOR);
	}

	/**
	 * Selects the next week
	 */

	private void nextWeek() {
		int selection = model.getSelectedWeek();
		if (isControlKeyDown == false) {
			deselectCell(selection, 0);
			controller.handle(CalendarAction.UNSELECT_WEEK);
		}
		if (selection < 5) {
			controller.increaseSelectedWeek();
		} else {
			controller.handle(CalendarAction.NEXT_MONTH);
			controller.setSelectedWeekLine(0);
		}
		selectCell(model.getSelectedWeek(), 0);
		controller.handle(CalendarAction.SELECT_WEEK_CURSOR);
	}

	/**
	 * Selects a week in the grid
	 * 
	 * @param n
	 */

	private void selectWeek(int n) {
		controller.handle(CalendarAction.UNSELECT_WEEK);
		controller.setSelectedWeekLine(n);
		controller.handle(CalendarAction.SELECT_WEEK_CURSOR);
	}

	/**
	 * Manages key down events
	 */

	@Override
	public void onKeyDown(KeyDownEvent event) {
		if (model.getSelectionType() != SelectionType.WEEK) {
			return;
		}
		int keyCode = event.getNativeKeyCode();
		int month = model.getDisplayedMonth();
		int year = model.getDisplayedYear();

		switch (keyCode) {
		case KeyCodes.KEY_UP:
			previousWeek();
			break;
		case KeyCodes.KEY_DOWN:
			nextWeek();
			break;
		case KeyCodes.KEY_HOME:
			selectWeek(0);
			break;
		case KeyCodes.KEY_END:
			selectWeek(CalendarUtils.getWeekNumberInMonth(month, year));
			break;
		case KeyCodes.KEY_CTRL:
			isControlKeyDown = true;
			break;
		case KeyCodes.KEY_SHIFT:
			isShiftKeyDown = true;
			break;
		}
	}

	/**
	 * Manages key up events
	 */

	@Override
	public void onKeyUp(KeyUpEvent event) {
		if (model.getSelectionType() != SelectionType.WEEK) {
			return;
		}
		int keyCode = event.getNativeKeyCode();
		if (keyCode == KeyCodes.KEY_CTRL) {
			isControlKeyDown = false;
		} else if (keyCode == KeyCodes.KEY_SHIFT) {
			isShiftKeyDown = false;
		}
	}

	@Override
	public void onMouseWheel(MouseWheelEvent event) {
		SelectionType type = model.getSelectionType();
		if (type != SelectionType.WEEK) {
			return;
		}
		event.preventDefault();
		isControlKeyDown = event.isControlKeyDown();
		isShiftKeyDown = event.isShiftKeyDown();
		if (event.isNorth() == true) {
			previousWeek();
		} else if (event.isSouth() == true) {
			nextWeek();
		}
	}
}
